/**
* Copyright (c) 2010, Attila Magyar
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the Attila Magyar nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.uftp.buffers.impl;

import java.io.IOException;
import java.io.InputStream;
import org.uftp.log.FTPLogManager;
import org.uftp.log.FTPLogger;

/**
 *
 * @author Zeroflag
 */
public class CircularBuffer extends SimpleReader {
    
    private static FTPLogger logger = FTPLogManager.getDefaultLogger( CircularBuffer.class.getName() );
    
    private byte[] buffer;
    private int start, end;
    
    private boolean full;    

    /** Creates a new instance of BufferReader */
    public CircularBuffer() {        
        this(null);
    }
    
    /** Creates a new instance of BufferReader */
    public CircularBuffer( InputStream stream ) {        
        super( stream );
        clear();    
        logger.debug( "Using buffer implementation: " + this.getClass().getName() );
    }
        
    public void init(int bufferSize) {
        buffer = new byte[ bufferSize ];
    }
    
    static int min( int x, int y ) { return x < y ? x : y; }
    
    public boolean isFull() {
        return full;
    }

    public boolean isEmpty() {
        return !full && start == end ;
    }
    
    public int getUsedSpace() {
        return buffer.length - getFreeSpace();
    }
    
    public void clear() {
        start = end = 0; 
        full = false;
    }
    
    public int getFreeSpace() {
        if ( isEmpty() ) {
            return buffer.length;
        }
        if ( isFull() ) {
            return 0;
        }
        if ( start > end ) {            
            return start - end;  
        }
        else {
            return buffer.length - end + start;
        }
    }
    
    private int read( byte[] buff, int count ) {
        
        if ( isEmpty() || count <= 0 ) {
            return 0;
        }
        if ( start >= end  ) {
            int remSpace = buffer.length - start;
            
            if ( count <= remSpace  ) {
                System.arraycopy( buffer, start, buff, 0, count );                
                start = (start + count) % buffer.length; // if count=remspace start <- 0
                full = false;
                return count;
            }
            System.arraycopy( buffer, start, buff, 0, remSpace );
            count -= remSpace;            
            int canRead = min( count, end );
            System.arraycopy( buffer, 0, buff, remSpace, canRead );
            start = canRead;
            full = false;
            return remSpace + canRead;
        }
        else {
            // cannot be full, setting isFull is not necessary
            int canRead = min( count, end - start );
            System.arraycopy( buffer, start, buff, 0, canRead );
            start = (start + canRead) % buffer.length; // modulo not necessary here
            return canRead;
        }
            
    }
            
    public String readLine() throws IOException {

        if ( getFreeSpace() > 2 * (buffer.length / 3) && stream.available() > 0 ) {            
            readStream();
        }
                
        String line = readLineFromBuffer();
        
        if ( line == null ) {

            int res = readStream();            
            if ( res != -1 ) {
                line = readLineFromBuffer();
            }
            
            if ( line == null && isFull() ) { // no line found, and buffer is full, try to search CRLF without buffering
                // buffer is full, so start == end                
                StringBuffer buf = 
                    new StringBuffer( new String( this.buffer, start, buffer.length - start ) );                        
                buf.append( new String( this.buffer, 0, end ) );                
                
                return readLineByChar( buf );                                                
            }
           
            if ( line == null && res == -1 ) { // no line found, stream ended, return the current content of the buffer
                
                if ( isEmpty() ) return null;
                
                // return the rest of the buffer
                if ( start < end ) {
                    return new String( this.buffer, start, end - start );
                }
                else {
                    StringBuffer buf = 
                        new StringBuffer( new String( this.buffer, start, buffer.length - start ) );                        
                    buf.append( new String( this.buffer, 0, end ) );                
                    
                    return buf.toString();                    
                }
            }
        }
        
        return line;
    }
    
    private String readLineFromBuffer() throws IOException {
        if ( isEmpty() ) {
            return null;
        }
        
        int e;
        if ( start >= end ) {
            e = buffer.length;
        }
        else {
            e = end;
        }        
        
        boolean crFound = false;
        int i;
        for ( i = start; i < e; i++ ) {
                                    
            if ( buffer[i] == LF && crFound ) {
                byte[] b = new byte[ i - start +1];
                read( b, i - start +1 );      
                return new String(b, 0, b.length -2);
            }            

            crFound = buffer[ i ] == CR;
        }                                
        
        if ( start >= end ) { 
            
            for ( i = 0; i < end; i++ ) {
                
                if ( buffer[i] == LF && crFound ) {

                    int len = buffer.length - start + i +1;
                    // + elozo korbol maradt
                    byte[] b = new byte[ len ];                    
                    read( b, len );                     
                    return new String(b, 0, b.length -2);
                }            
   
                crFound = buffer[ i ] == CR;
            }
    
            if ( crFound ) {
                // |-------CR/E______S--------|
                int next = stream.read();
                if ( next == LF ) {
                    int len = buffer.length - start + end;
                    byte[] b = new byte[ len ];                    
                    read( b, len );                     
                    return new String(b, 0, b.length -1);
                }
                else {                    
                                                       
                    StringBuffer buf = 
                        new StringBuffer( new String( this.buffer, start, buffer.length - start ) );                        
                    buf.append( new String( this.buffer, 0, end ) );
                    
                    if ( next == -1 ) {
                        clear();
                        return buf.toString();
                    }
                    else {
                        buf.append( (char)next );                  
                        return readLineByChar( buf );
                    }
                }
            }
        }        
        else if ( crFound ) {
            // |__________S-------CR/E______|
            int next = stream.read();
            if ( next == LF ) {
                byte[] b = new byte[ end - start ];
                read( b, end - start );      
                return new String(b, 0, b.length -1);                
            }   
            else {
                StringBuffer buf = 
                    new StringBuffer( new String( this.buffer, start, end - start ) );                        
                
                if ( next == -1 ) {
                    clear();
                    return buf.toString();
                }
                else {
                    buf.append( (char)next );
                    return readLineByChar( buf );                
                }

            }
        }
        
        return null;
    }
        
    private int readStream() throws IOException {
                
        int count = 0;
        if ( isFull() ) {
            return -1;
        }
                
        // end is exclusive
        if ( start > end ) {            
            // [-------E_____S----]            
            int canRead = start - end;  
            
            count = stream.read( buffer, end, canRead );
            
            if ( count < 0 ) {
                return -1;
            }
            else {
                end += count;
            }

            if ( start == end ) {
                full = true;
            }
                        
        }
        else { // end > start or end == start   [______S-----E_____]  or  [____SE___________]
            
            //int fullFree = buffer.length - end + start;
            int remainSpace = buffer.length -end;
                       
            count += stream.read( buffer, end, remainSpace );
            
            if ( count < 0 ) {
                return -1;
            }
            else {
                end += count;
            }
            
            int aval = stream.available();
            if ( remainSpace == count ) { 
                int c = stream.read( buffer, 0, min( aval, start ) );
                if ( c >= 0 ) {
                    end = c;
                    count += c;
                }
            }
            
            if ( start == end ) {
                full = true;
            }       
        }
        
        return count;
    }
   
    
    protected String readLineByChar(StringBuffer buf) throws IOException {        
        String ret = super.readLineByChar(buf);
        clear();
        return ret;
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append( "s=" + start + " e=" + end + " l=" + buffer.length + " f=" + getFreeSpace() + " empty:" + isEmpty() + " full:" + isFull() );
        sb.append( "\n" );
        for ( int i = 0; i < buffer.length; i++ ) {
            if ( i == start ) {
                sb.append( "[S]" + (char)buffer[i] );
            }
            else if ( i == end ) {
                sb.append( "[E]" + (char)buffer[i] );
            }
            else {
                sb.append( (char)buffer[i]  );
            }
            sb.append( "," );
        }
        return sb.toString();
    }    
}
